/*
 * This file is part of LibrePlan
 *
 * Copyright (C) 2009-2010 Fundación para o Fomento da Calidade Industrial e
 *                         Desenvolvemento Tecnolóxico de Galicia
 * Copyright (C) 2010-2011 Igalia, S.L.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.libreplan.web.materials;

import static org.libreplan.business.common.exceptions.ValidationException.invalidValue;
import static org.libreplan.web.I18nHelper._;

import java.util.ArrayList;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.libreplan.business.common.IntegrationEntity;
import org.libreplan.business.common.daos.IConfigurationDAO;
import org.libreplan.business.common.entities.EntityNameEnum;
import org.libreplan.business.common.exceptions.InstanceNotFoundException;
import org.libreplan.business.common.exceptions.ValidationException;
import org.libreplan.business.materials.daos.IMaterialAssignmentDAO;
import org.libreplan.business.materials.daos.IMaterialCategoryDAO;
import org.libreplan.business.materials.daos.IMaterialDAO;
import org.libreplan.business.materials.daos.IUnitTypeDAO;
import org.libreplan.business.materials.entities.Material;
import org.libreplan.business.materials.entities.MaterialCategory;
import org.libreplan.business.materials.entities.UnitType;
import org.libreplan.web.common.IntegrationEntityModel;
import org.libreplan.web.common.concurrentdetection.OnConcurrentModification;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.zkoss.ganttz.util.MutableTreeModel;

/**
 * @author Vova Perebykivskyi <vova@libreplan-enterprise.com>
 */
@Service
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
@OnConcurrentModification(goToPage = "/materials/materials.zul")
public class MaterialsModel extends IntegrationEntityModel implements IMaterialsModel {

    @Autowired
    IMaterialCategoryDAO categoryDAO;

    @Autowired
    IMaterialDAO materialDAO;

    @Autowired
    IUnitTypeDAO unitTypeDAO;

    @Autowired
    IConfigurationDAO configurationDAO;

    @Autowired
    IMaterialAssignmentDAO materialAssignmentDAO;

    MutableTreeModel<MaterialCategory> materialCategories = MutableTreeModel.create(MaterialCategory.class);

    private List<UnitType> unitTypes = new ArrayList<>();

    private MaterialCategory currentMaterialCategory;

    private Map<MaterialCategory, String> oldCodes = new HashMap<>();

    private Map<Material, String> oldMaterialCodes = new HashMap<>();

    @Override
    @Transactional(readOnly = true)
    public MutableTreeModel<MaterialCategory> getMaterialCategories() {
        if (materialCategories.isEmpty()) {
            initializeMaterialCategories();
        }
        return materialCategories;
    }

    @Override
    @Transactional(readOnly=true)
    public void reloadMaterialCategories() {
        materialCategories = MutableTreeModel.create(MaterialCategory.class);
        initializeMaterialCategories();
    }

    private void initializeMaterialCategories() {
        final List<MaterialCategory> categories = categoryDAO.getAllRootMaterialCategories();

        for (MaterialCategory materialCategory: categories) {
            initializeMaterials(materialCategory.getMaterials());
            materialCategories.addToRoot(materialCategory);
            addCategories(materialCategory, materialCategory.getSubcategories());
            storeOldCodes(materialCategory);
        }
    }

    private void storeOldCodes(MaterialCategory materialCategory) {

        /* It stores temporally the autogenerated code */

        if (materialCategory.isCodeAutogenerated()) {
            oldCodes.put(materialCategory, materialCategory.getCode());
            for (Material child : materialCategory.getMaterials()) {
                oldMaterialCodes.put(child, child.getCode());
            }
        }
    }

    private void initializeMaterials(Set<Material> materials) {
        for (Material each: materials) {
            each.getDescription();
            if (each.getUnitType() != null) {
                each.getUnitType().getMeasure();
            }
        }
    }

    private void addCategories(MaterialCategory materialCategory, Set<MaterialCategory> categories) {
        for (MaterialCategory category: categories) {
            initializeMaterials(category.getMaterials());
            materialCategories.add(materialCategory, category);
            storeOldCodes(category);

            final Set<MaterialCategory> subcategories = category.getSubcategories();

            if (subcategories != null) {
                addCategories(category, subcategories);
            }
        }
    }

    @Override
    @Transactional(readOnly=true)
    public List<Material> getMaterials(MaterialCategory materialCategory) {
        List<Material> result = new ArrayList<>();
        result.addAll(materialCategory.getMaterials());

        return result;
    }

    @Override
    @Transactional(readOnly=true)
    public void addMaterialCategory(MaterialCategory parent, String categoryName) throws ValidationException {
        Validate.notNull(categoryName);

        Boolean generateCode = configurationDAO.getConfiguration().getGenerateCodeForMaterialCategories();
        MaterialCategory child = MaterialCategory.createUnvalidated("", _(categoryName));
        if ( generateCode ) {
            setCurrentMaterialCategory(child);
            setDefaultCode();
        }
        child.setCodeAutogenerated(generateCode);

        final MaterialCategory materialCategory = findMaterialCategory(child);
        if ( materialCategory != null ) {
            throw new ValidationException(invalidValue(
                    _("{0} already exists", materialCategory.getName()),
                    "name", materialCategory.getName(), materialCategory));
        }

        child.setParent(parent);
        if ( parent == null ) {
            materialCategories.addToRoot(child);
        } else {
            materialCategories.add(parent, child);
        }
    }

    private MaterialCategory findMaterialCategory(final MaterialCategory category) {
        for (MaterialCategory mc : materialCategories.asList()) {
            if ( equalsMaterialCategory(mc, category) ) {
                return mc;
            }
        }

        return null;
    }

    private boolean equalsMaterialCategory(MaterialCategory obj1, MaterialCategory obj2) {
        String name1 = StringUtils.deleteWhitespace(obj1.getName().toLowerCase());
        String name2 = StringUtils.deleteWhitespace(obj2.getName().toLowerCase());

        return name1.equals(name2);
    }

    @Override
    @Transactional
    public void confirmRemoveMaterialCategory(MaterialCategory materialCategory) {
        /* Remove from list of material categories */
        materialCategories.remove(materialCategory);

        /* Remove from its parent */
        final MaterialCategory parent = materialCategory.getParent();
        if (parent != null) {
            materialCategory.getParent().removeSubcategory(materialCategory);
        }

        final Long idMaterialCategory = materialCategory.getId();
        /* It's not a yet-to-save element */
        if (idMaterialCategory != null) {

            /* It has a parent, in this case is enough with saving parent (all-delete-orphan) */
            if (parent != null) {
                categoryDAO.save(materialCategory.getParent());
            } else {
                /* It was a root element, should be deleted from DB */
                try {
                    categoryDAO.remove(idMaterialCategory);
                } catch (InstanceNotFoundException e) {
                    throw new RuntimeException();
                }
            }
            reloadMaterialCategories();
        }
    }

    @Override
    public void addMaterialToMaterialCategory(MaterialCategory materialCategory) {
        Material material = Material.create("");
        material.setCategory(materialCategory);
        materialCategory.addMaterial(material);
    }

    @Override
    @Transactional
    public void confirmSave() throws ValidationException {
        final List<MaterialCategory> categories = materialCategories.asList();

        checkNoCodeRepeatedAtNewMaterials(categories);
        Integer numberOfDigits = getNumberOfDigitsCode();
        generateMaterialCodesIfIsNecessary(categories, numberOfDigits);

        for (MaterialCategory each : categories) {
            categoryDAO.save(each);
        }
    }

    private void generateMaterialCodesIfIsNecessary(List<MaterialCategory> categories, Integer numberOfDigits) {
        for (MaterialCategory category: categories) {
            if ( category.isCodeAutogenerated() ) {
                category.generateMaterialCodes(numberOfDigits);
            }
        }
    }

    private void checkNoCodeRepeatedAtNewMaterials(final List<MaterialCategory> categories) throws ValidationException {
        List<Material> allMaterials = MaterialCategory.getAllMaterialsWithoutAutogeneratedCodeFrom(categories);
        Map<String, Material> byCode = new HashMap<>();

        for (Material each : allMaterials) {
            if ( byCode.containsKey(each.getCode()) ) {
                throw new ValidationException(sameCodeMessage(each, byCode.get(each.getCode())));
            }
            byCode.put(each.getCode(), each);
        }
    }

    private String sameCodeMessage(Material first, Material second) {
        return _(
                "both {0} of category {1} and {2} of category {3} have the same code",
                asStringForUser(first), first.getCategory().getName(),
                asStringForUser(second), second.getCategory().getName());
    }

    private String asStringForUser(Material material) {
        return String.format("{code: %s, description: %s}", material.getCode(), material.getDescription());
    }

    @Override
    public void removeMaterial(Material material) {
        material.getCategory().removeMaterial(material);
    }

    @Override
    @Transactional(readOnly = true)
    public Collection<? extends Material> getMaterials() {
        List<Material> result = new ArrayList<>();

        for (MaterialCategory each: getMaterialCategories().asList()) {
            result.addAll(each.getMaterials());
        }

        return result;
    }

    @Override
    @Transactional(readOnly = true)
    public void loadUnitTypes() {
        List<UnitType> result = new ArrayList<>();

        for (UnitType each : unitTypeDAO.findAll()) {
            each.getMeasure();
            result.add(each);
        }

        this.unitTypes = result;
    }

    public List<UnitType> getUnitTypes() {
        return this.unitTypes;
    }

    @Override
    @Transactional(readOnly = true)
    public boolean canRemoveMaterial(Material material) {
        return material.isNewObject() || materialAssignmentDAO.getByMaterial(material).size() == 0;
    }

    @Override
    protected void restoreOldCodes() {
        getCurrentEntity().setCode(oldCodes.get(getCurrentEntity()));

        for (Material child : ((MaterialCategory) getCurrentEntity()).getMaterials()) {
            if (!child.isNewObject()) {
                child.setCode(oldMaterialCodes.get(child));
            }
        }
    }

    public void setCodeAutogenerated(boolean codeAutogenerated,
                                     MaterialCategory materialCategory) throws ConcurrentModificationException {

        setCurrentMaterialCategory(materialCategory);
        setCodeAutogenerated(codeAutogenerated);
    }

    @Override
    public Boolean isGenerateCodeOld() {
        return getCurrentEntity() != null && oldCodes.get(getCurrentEntity()) != null;
    }

    public EntityNameEnum getEntityName() {
        return EntityNameEnum.MATERIAL_CATEGORY;
    }

    public Set<IntegrationEntity> getChildren() {
        return new HashSet<>();
    }

    public IntegrationEntity getCurrentEntity() {
        return this.currentMaterialCategory;
    }

    public void setCurrentMaterialCategory(MaterialCategory materialCategory) {
        this.currentMaterialCategory = materialCategory;
    }

}
